package util;

import auto.BoordComputer;
import auto.Location;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import rekeningrijdenserver.RekeningRijdenServer;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import tijdserver.TijdServerImpl;
import util.Message.MessageType;

/**
 *
 * @author Sam en Jonas
 */
public class SecurityManager {
    // static data

    RekeningRijdenServer server;
    public BoordComputer boordComputer;
    // local keys
    public PublicKey ownPublicKey;
    private PrivateKey ownPrivateKey;
    // TODO : Waar vind ik de ID-value van de auto?
    static final byte[] ID = ("SomeStaticKeyID").getBytes();
    // server key
    public PublicKey otherPublicKey;
    // Nonce
    byte[] nonce;
    // CipherSpec from SecretKey
    SecretKey secret;

    // constructor
    public SecurityManager(BoordComputer bc) {
//        server = bc.getServer();
        boordComputer = bc;
        GenerateOwnKeys();
    }

    // keypair generation
    private void GenerateOwnKeys() {

        try {

            // Generate Assymetric keypair via RSA with 2048 bits
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(2048);
            KeyPair kp = kpg.genKeyPair();

            // store keys
            ownPublicKey = kp.getPublic();
            ownPrivateKey = kp.getPrivate();

        } catch (NoSuchAlgorithmException e) {
        }
    }

    // getters & setters
    public PublicKey getOwnPublicKey() {
        return ownPublicKey;
    }

    public void setOwnPublicKey(PublicKey publicKey) {
        this.ownPublicKey = publicKey;
    }

    // create nonce
    private static byte[] createNonce() {
        byte[] N = new byte[16];

        try {
            Random rand;
            rand = SecureRandom.getInstance("SHA1PRNG");
            rand.nextBytes(N);

        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(SecurityManager.class.getName()).log(Level.SEVERE, null, ex);
        }
        return N;
    }
    
    public void setCypherSuite(String something){
        // Intentionally left empty
        // TODO : leeglaten? Gewoon aantonen met  SHA1 / AES ?
    }
    
    /*public void clientHello() {
        this.nonce = createNonce();
        HashMap<String,Object> hm = new HashMap<String, Object>();
        hm.put("client_nonce",nonce.toString());
        hm.put("RSA","2048");
        hm.put("SHA-256","");
        hm.put("AES","128");
        server.sendMessage(new Message(MessageType.CLIENT_HELLO, hm), this.boordComputer);
    }*/
    public static String generateNonce(){
        return new String(createNonce());
    }
    
    /*public void serverHello() {
        HashMap<String,Object> hm = new HashMap<String,Object>();
        hm.put("response","ok");
        this.boordComputer.sendMessage(new Message(MessageType.SERVER_HELLO,hm), server);
        sendServerCertificate();
    }*/
    
    /*public void sendServerCertificate(){
        HashMap<String,Object> hm = new HashMap<String,Object>();
        hm.put("certificate", new Certificate(ownPublicKey,null));
        this.boordComputer.sendMessage(new Message(MessageType.SERVER_CERT,hm), server);
    }*/
    public static Certificate generateCertificate(String keyLocation) throws IOException{
        return new Certificate(readPublicKeyFromFile(keyLocation),null);
    }
    
    /*public void sendClientCertificate(){
        HashMap<String,Object> hm = new HashMap<String,Object>();
        hm.put("certificate", new Certificate(ownPublicKey,null));
        server.sendMessage(new Message(MessageType.CLIENT_CERT,hm), this.boordComputer);
        clientKeyExchange();
    }*/

    public static byte[] generateSalt(){
        Random r = new SecureRandom();
        //make salt
        // --> Share this with server
        byte[] salt = new byte[8];
        r.nextBytes(salt);

        return salt;
    }
    public static char[] generatePreMasterSecret(){
        Random r = new SecureRandom();
        // make pre-master-secret
        // --> Share this with server
        byte[] pms = new byte[8];
        r.nextBytes(pms);

        return new String(pms).toCharArray();
    }
    /*public void clientKeyExchange(){
        
        Random r = new SecureRandom();
        //make salt
        // --> Share this with server
        byte[] salt = new byte[8];
        r.nextBytes(salt);
        // make pre-master-secret
        // --> Share this with server
        byte[] pms = new byte[8];
        r.nextBytes(pms);
        char[] pre_master_secret = pms.toString().toCharArray();

        setSecretKey(pre_master_secret,salt);
    
        HashMap<String,Object> hm = new HashMap<String,Object>();
        hm.put("salt",salt);
        hm.put("pre_master_secret",pre_master_secret);
        
        // TODO :  encrypt key_exchange with publicServerKey
        server.sendMessage(new Message(MessageType.CLIENT_KEY_EXCHANGE,hm), this.boordComputer);
    }*/
    
    public static SecretKeySpec getSecretKey(char[] pms,byte[] salt){
        SecretKeyFactory factory;
        KeySpec spec;
        SecretKey tmp;
        try{
            factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            spec = new PBEKeySpec(pms, salt, 1024, 128);
            tmp = factory.generateSecret(spec);
            //this.secret = new SecretKeySpec(tmp.getEncoded(), "AES");
            return new SecretKeySpec(tmp.getEncoded(), "AES");
        }catch(Exception e){
            Logger.getLogger(SecurityManager.class.getName()).log(Level.SEVERE, null, e);
            return null;
        }
    }

    /**
     * Deze methode zal GPSData omzetten in een geencrypteerde vorm
     * @param timestamp de tijd horend bij een locatie
     * @param location de locatie die opgeslagen wordt
     * @param keyValue de gebruikte sleutel
     * @return Een stringrepresentatie van de geencrypteerde GPSData
     * @throws Exception
     */
    public static String encryptRawGPSData(Long timestamp, Location location, String keyLocation) throws Exception {
        Key key = readSecretKeyFromFile(keyLocation);
        Cipher c = Cipher.getInstance("AES");
        c.init(Cipher.ENCRYPT_MODE, key);

        ByteBuffer bb = ByteBuffer.wrap(new byte[Long.SIZE + 2 * Double.SIZE]);
        bb.putLong(timestamp);
        bb.putDouble(location.getCoordinates()[0]);
        bb.putDouble(location.getCoordinates()[1]);
        byte[] valueToEnc = bb.array();

        byte[] encValue = c.doFinal(valueToEnc);
        String encryptedValue = new BASE64Encoder().encode(encValue);
        return encryptedValue;
    }

    public static Message encryptMessage(Key usedKey,Message msg) throws Exception {
        if(usedKey instanceof SecretKey){
            Key key = usedKey;
            Cipher c = Cipher.getInstance("AES");
            c.init(Cipher.ENCRYPT_MODE, key);

            byte[] valueToEnc = toByteArray(msg);
            byte[] encValue = c.doFinal(valueToEnc);
            String encryptedValue = new BASE64Encoder().encode(encValue);
            HashMap<String, Object> encryptedMessage = new HashMap<String, Object>();
            encryptedMessage.put("encrypted", encryptedValue);
            return new Message(MessageType.ENCRYPTED, encryptedMessage);
        }
        if(usedKey instanceof PublicKey){
            PublicKey key = (PublicKey)usedKey;

            ArrayList<byte[]> encArray = new ArrayList<byte[]>();
            encArray = asymmetricEncrypt(toByteArray(msg), key);
            
            HashMap<String, Object> encryptedMessage = new HashMap<String, Object>();
            encryptedMessage.put("encrypted", encArray);
            return new Message(MessageType.PRE_ENCRYPTED, encryptedMessage);
        }
        return null;
    }

    /**
     * Deze methode zal de geencrypteerde GPSData terug omzetten naar locaties
     * @param encrypted De geencrypteerde GPSData
     * @param keyValue De gebruikte sleutel
     * @return Een HashMap van een timestamp met bijhorende locatie
     * @throws Exception
     */
    public static HashMap<Long, Location> decryptRawGPSData(String encrypted, String keyLocation) throws Exception {
        Key key = readSecretKeyFromFile(keyLocation);
        Cipher c = Cipher.getInstance("AES");
        c.init(Cipher.DECRYPT_MODE, key);
        byte[] decodedValue = new BASE64Decoder().decodeBuffer(encrypted);
        byte[] decValue = c.doFinal(decodedValue);

        ByteBuffer bb = ByteBuffer.wrap(decValue);
        long timestamp = bb.getLong();
        double lat = bb.getDouble();
        double lon = bb.getDouble();

        HashMap<Long, Location> returnHash = new HashMap<Long, Location>();
        returnHash.put(timestamp, new Location(lat, lon));
        return returnHash;
    }

    public static Message decryptMessage(Key usedKey, Message encryptedMsg) throws Exception {
        if(usedKey instanceof SecretKey){
            String encrypted = (String)encryptedMsg.getData().get("encrypted");
            Key key = usedKey;
            Cipher c = Cipher.getInstance("AES");
            c.init(Cipher.DECRYPT_MODE, key);
            byte[] decodedValue = new BASE64Decoder().decodeBuffer(encrypted);
            byte[] decValue = c.doFinal(decodedValue);

            Message decodedMessage = (Message)toObject(decValue);
            return decodedMessage;
        }
        if(usedKey instanceof PrivateKey){
            PrivateKey key = (PrivateKey)usedKey;
            byte[] decrypted = asymmetricDecrypt((ArrayList<byte[]>)encryptedMsg.getData().get("encrypted"), key);
            Message msg = (Message)toObject(decrypted);
            return msg;
        }
        return null;
    }

    /**
     * Ondertekenen van data: deze methode zal een lijst van objecten Hashen,
     * en zal daarna deze Hash gaan encrypteren en de bytes teruggeven
     * @param data De lijst die moet getekend worden
     * @param keyLocation De locatie van de te gebruiken sleutel
     * @return de bytes van de signature
     */
    public static byte[] signData(ArrayList<Object> data,String keyLocation){
        try {
            byte[] hash = hashData(data,"SHA-256");
            return asymmetricEncrypt(hash,keyLocation);
        } catch (Exception ex) {
            Logger.getLogger(TijdServerImpl.class.getName()).log(Level.SEVERE, null, ex);
            return null;
        }
    }

    /**
     * Deze methode zal controleren of de geleverde data effectief correct getekend is
     * @param signed Deze data werd extern ook getekend
     * @param keyLocation de publieke sleutel om te decrypteren
     * @param signature de signatuur die extern berekend werd
     * @return
     */
    public static boolean verifyData(ArrayList<Object> signed, String keyLocation, byte[] signature) {
        try {
            byte[] hash = hashData(signed, "SHA-256");
            byte[] decrypted = asymmetricDecrypt(signature, keyLocation);

            return Arrays.equals(hash, decrypted);
        } catch (Exception ex) {
            Logger.getLogger(SecurityManager.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
    }

    private static byte[] hashData(ArrayList<Object> data,String algo) throws NoSuchAlgorithmException{
        MessageDigest md = MessageDigest.getInstance(algo);
        for(Object obj : data)
            md.update(toByteArray(obj));
        return md.digest();
    }
    private static byte[] asymmetricEncrypt(byte[] data,String keyLocation) throws Exception{
        PrivateKey privateKey = readPrivateKeyFromFile(keyLocation);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte[] cipherData = cipher.doFinal(data);
        return cipherData;
    }
    private static ArrayList<byte[]> asymmetricEncrypt(byte[] data,PublicKey key) throws Exception{
        ArrayList<byte[]> returnList = new ArrayList<byte[]>();
        PublicKey publicKey = key;
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);

        for(int i=0;i<data.length/245;i++){
            byte[] cipherData = cipher.doFinal(data, i*245, 245);
            returnList.add(cipherData);
        }
        byte[] cipherData = cipher.doFinal(data, data.length-data.length%245, data.length%245);
        returnList.add(cipherData);
        return returnList;
    }
    private static byte[] asymmetricDecrypt(byte[] encrypted,String keyLocation) throws Exception{
        PublicKey publicKey = readPublicKeyFromFile(keyLocation);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] cipherData = cipher.doFinal(encrypted);
        return cipherData;
    }
    private static byte[] asymmetricDecrypt(ArrayList<byte[]> encrypted,PrivateKey key) throws Exception{
        PrivateKey privateKey = key;
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        ArrayList<byte[]> decryption = new ArrayList<byte[]>();

        int size=0;
        for(byte[] b:encrypted){
            byte[] partialDecryption = cipher.doFinal(b);
            decryption.add(partialDecryption);
            size+=partialDecryption.length;
        }

        byte[] totalDecryption = new byte[size];
        size=0;
        for(byte[] bytes : decryption){
            System.arraycopy(bytes, 0, totalDecryption, size, bytes.length);
            size+=bytes.length;
        }

        return totalDecryption;
    }

    public static PrivateKey readPrivateKeyFromFile(String keyFileName) throws IOException {
        InputStream in = new FileInputStream(keyFileName);
        ObjectInputStream oin = new ObjectInputStream(new BufferedInputStream(in));
        try {
            BigInteger m = (BigInteger) oin.readObject();
            BigInteger e = (BigInteger) oin.readObject();
            RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(m, e);
            KeyFactory fact = KeyFactory.getInstance("RSA");
            PrivateKey privateKey = fact.generatePrivate(keySpec);
            return privateKey;
        } catch (Exception e) {
            throw new RuntimeException("Spurious serialisation error", e);
        } finally {
            oin.close();
        }
    }
    public static PublicKey readPublicKeyFromFile(String keyFileName) throws IOException {
        InputStream in = new FileInputStream(keyFileName);
        ObjectInputStream oin = new ObjectInputStream(new BufferedInputStream(in));
        try {
            BigInteger m = (BigInteger) oin.readObject();
            BigInteger e = (BigInteger) oin.readObject();
            RSAPublicKeySpec keySpec = new RSAPublicKeySpec(m, e);
            KeyFactory fact = KeyFactory.getInstance("RSA");
            PublicKey publicKey = fact.generatePublic(keySpec);
            return publicKey;
        } catch (Exception e) {
            throw new RuntimeException("Spurious serialisation error", e);
        } finally {
            oin.close();
        }
    }
    public static SecretKeySpec readSecretKeyFromFile(String fileName) throws IOException{
        ObjectInputStream oin = new ObjectInputStream(new BufferedInputStream(new FileInputStream(fileName)));
        try {
            SecretKeySpec key = (SecretKeySpec)oin.readObject();
            return key;
        } catch (Exception e) {
            throw new RuntimeException("Spurious serialisation error", e);
        } finally{
            oin.close();
        }
    }

    /**
     * Methode die een Object omzet naar een byte[]
     * @param obj het Object dat moet omgezet worden in bytes
     * @return de overeenkomstige bytes
     */
    public static byte[] toByteArray(Object obj) {
        byte[] bytes = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            oos.flush();
            oos.close();
            bos.close();
            bytes = bos.toByteArray();
        } catch (IOException ex) {
            System.err.println(ex);
        }
        return bytes;
    }

    /**
     * Methode die een byte[] omzet naar een Object
     * @param bytes bytes die omgezet moet worden
     * @return het overeenkomende Object
     */
    public static Object toObject(byte[] bytes) {
        Object obj = null;
        try {
            ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
            ObjectInputStream ois = new ObjectInputStream(bis);
            obj = ois.readObject();
        } catch (IOException ex) {
            //empty
        } catch (ClassNotFoundException ex) {
            //empty
        }
        return obj;
    }
}
